namespace hjc {

	export class WebSocket extends Singleton implements ILaunchable {

		private $configs: WSConfigs;
		private $buffer: egret.ByteArray;
		private $ews: egret.WebSocket;
		private $refused: boolean;		//服务器主动拒绝
		private $reconnected: boolean;	//断线重连
		private $protos: Proto[];
		private $map: Map<string, { observer: Proxy | Controller, handler: Function }>;
		private $suc: (rec: boolean) => any;
		private $failed: (refused: boolean) => any;
		public set protos(protos: Proto[]) { this.$protos = protos; }
		public get protos(): Proto[] { return this.$protos; }

		public register(proto: Proto, handler: Function, observer?: Proxy | Controller): void {
			let name = utils.nameOfClazz(proto);
			if (this.$map.has(name)) wrong('WebSocket.register()', `duplicate register msg, id:${proto}`);
			this.$map.set(name, { observer, handler });
		}

		public launch(options: LaunchOptions): void {
			let configs = options.ws
			if (notEmpty(configs)) {
				if (!configs.url && !numeric(configs.port) || !!configs.host) wrong('WebSocket.launch()', 'both empty', 'url or host&port');
				else {
					this.$configs = configs;
					this.$ews = new egret.WebSocket;
					this.$buffer = new egret.ByteArray;
					if ([PBHeadLen.BIT8, PBHeadLen.BIT16, PBHeadLen.BIT32].excludes(configs.pbHeadLen)) this.$configs.pbHeadLen = PBHeadLen.BIT16;
					this.$buffer.endian = this.$configs.bigEndian === false ? egret.Endian.LITTLE_ENDIAN : egret.Endian.BIG_ENDIAN;
					if (notEmptyArr(configs.protos)) this.$protos = configs.protos;
					this.$map = new Map;
					this.$ews.addEventListener(egret.Event.CONNECT, this._onConnected, this);
					this.$ews.addEventListener(egret.ProgressEvent.SOCKET_DATA, this._onReceived, this);
					this.$ews.addEventListener(egret.Event.CLOSE, this._onClosed, this);
					this.$ews.addEventListener(egret.IOErrorEvent.IO_ERROR, this._onError, this);
					this.$configs.type = configs.type === DataType.JSON ? DataType.JSON : DataType.PROTOBUF;
					this.$ews.type = this.$configs.type;
				}
			}
		}

		public connect(suc?: (reconnected: boolean) => void, failed?: (refused: boolean) => void): void {
			this.$refused = true;
			this.$reconnected = false;
			this.$suc = suc;
			this.$failed = failed;
			if (!!this.$configs.url) this.$ews.connectByUrl(this.$configs.url);
			else this.$ews.connect(this.$configs.host, this.$configs.port);
		}

		public close(): void {
			if (!!this.$ews) {
				this.$ews.removeEventListener(egret.Event.CONNECT, this._onConnected, this);
				this.$ews.removeEventListener(egret.ProgressEvent.SOCKET_DATA, this._onReceived, this);
				this.$ews.removeEventListener(egret.Event.CLOSE, this._onClosed, this);
				this.$ews.removeEventListener(egret.IOErrorEvent.IO_ERROR, this._onError, this);
				this.$ews.close();
			}
		}

		public send(proto: Proto, data: Object): void {
			if (!!this.$ews && !!this.$ews.connected) {
				if (!notEmptyArr(this.$protos)) { hjc.wrong('WebSocket.send()', 'protos not defined!'); return; }
				let id = this.$protos.indexOf(proto);
				if (id < 0) { hjc.wrong('WebSocket.send()', 'can\'t find proto', utils.nameOfClazz(proto)); return; }
				this.$buffer.clear();
				if (this.$configs.pbHeadLen === PBHeadLen.BIT8) this.$buffer.writeByte(id);
				else if (this.$configs.pbHeadLen === PBHeadLen.BIT16) this.$buffer.writeUnsignedShort(id);
				else this.$buffer.writeUnsignedInt(id);
				this.$buffer.writeBytes(new egret.ByteArray(proto.encode(data).finish()));
				this.$ews.writeBytes(this.$buffer);
				this.$ews.flush();
			} else warn('WebSocket.send()', 'failed to send msg...')
		}

		//发生错误时
		private _onError(e: egret.Event): void {
			if (isFunc(this.$failed)) this.$failed(false);
			this.$refused = false;
			// tools.cover.show('网络出错，暂无法连接到服务器！', 0xFF2222);
		}

		//在无网络(服务器宕机)时触发onError->onClosed
		//服务器主动踢时触发onClosed
		private _onClosed(e: egret.Event): void {
			if (!this.$refused) return;
			if (isFunc(this.$failed)) this.$failed(true);
			if (!this.$configs.autoReconnect !== false) this.connect(this.$suc, this.$failed);
		}

		private _onConnected(e: egret.Event): void {
			if (isFunc(this.$suc)) this.$suc(this.$reconnected);
			this.$reconnected = true;
		}

		private _onReceived(e: egret.Event): void {
			this.$buffer.clear();
			if (this.$configs.type === DataType.PROTOBUF) {
				if (!notEmptyArr(this.$protos)) { hjc.wrong('WebSocket.received()', 'protos not defined!'); return; }
				this.$ews.readBytes(this.$buffer);
				this.$buffer.position = 0;
				let [id, u8Arr] = [-1, null];
				if (this.$configs.pbHeadLen === PBHeadLen.BIT8) {
					id = this.$buffer.readUnsignedByte();
					u8Arr = this.$buffer.bytes.subarray(1, this.$buffer.length);
				} else if (this.$configs.pbHeadLen === PBHeadLen.BIT16) {
					id = this.$buffer.readUnsignedShort();
					u8Arr = this.$buffer.bytes.subarray(2, this.$buffer.length);
				} else {
					id = this.$buffer.readUnsignedInt();
					u8Arr = this.$buffer.bytes.subarray(4, this.$buffer.length);
				}
				let proto: Proto = this.$protos[id];
				if (!proto) { hjc.wrong('WebSocket.received()', 'can\'t find proto by id:', id + ''); return; }
				else this._handler(proto, u8Arr);
			} else {
				// let json = this.$ews.readUTF();
			}
		}

		private _handler(proto: Proto, u8Arr: Uint8Array): void {
			let name = utils.nameOfClazz(proto);
			if (!this.$map.has(name)) wrong('WebSocket.handler()', `unregistered proto received:${name}`);
			else {
				let ob = this.$map.get(name);
				utils.call(ob.handler, ob.observer, proto.decode(u8Arr));
			}
		}
	}
}
